Introduction à la modélisation statistique bayésienne

Un cours en R, Stan, et brms

Ladislas Nalborczyk (LPC, LNC, CNRS, Aix-Marseille Univ)

Planning

Cours n°01 : Introduction à l’inférence bayésienne
Cours n°02 : Modèle Beta-Binomial
Cours n°03 : Introduction à brms, modèle de régression linéaire
Cours n°04 : Modèle de régression linéaire (suite)
Cours n°05 : Markov Chain Monte Carlo
Cours n°06 : Modèle linéaire généralisé
Cours n°07 : Comparaison de modèles
Cours n°08 : Modèles multi-niveaux
Cours n°09 : Modèles multi-niveaux généralisés
Cours n°10 : Data Hackathon

Langage de la modélisation

\[ \begin{align} y_{i} &\sim \mathrm{Normal}(\mu_{i}, \sigma) \\ \mu_{i}&= \alpha + \beta x_{i} \\ \alpha &\sim \mathrm{Normal}(60, 10) \\ \beta &\sim \mathrm{Normal}(0, 10) \\ \sigma &\sim \mathrm{HalfCauchy}(0, 1) \end{align} \]

Objectif de la séance : comprendre ce type de modèle.

Les constituants de nos modèles seront toujours les mêmes et nous suivrons les trois mêmes étapes :

  • Construire le modèle (likelihood + priors).
  • Mettre à jour grâce aux données (updating), afin de calculer la distribution postérieure.
  • Interpréter les estimations du modèle, évaluer ses prédictions, éventuellement modifier le modèle.

Un premier modèle

library(rethinking)
library(tidyverse)

data(Howell1)
d <- Howell1
str(d)
'data.frame':   544 obs. of  4 variables:
 $ height: num  152 140 137 157 145 ...
 $ weight: num  47.8 36.5 31.9 53 41.3 ...
 $ age   : num  63 63 65 41 51 35 32 27 19 54 ...
 $ male  : int  1 0 0 1 0 1 0 1 0 1 ...
d2 <- d %>% filter(age >= 18)
head(d2)
   height   weight age male
1 151.765 47.82561  63    1
2 139.700 36.48581  63    0
3 136.525 31.86484  65    0
4 156.845 53.04191  41    1
5 145.415 41.27687  51    0
6 163.830 62.99259  35    1

Un premier modèle

\[h_{i} \sim \mathrm{Normal}(\mu, \sigma)\]

d2 %>%
    ggplot(aes(x = height) ) +
    geom_histogram(bins = 10, col = "white")

Loi normale

\[ p(x \ | \ \mu, \sigma) = \frac{1}{\sqrt{2 \pi \sigma^{2}}} \exp \bigg[-\frac{1}{2 \sigma^{2}} (\mu - x)^{2} \bigg] \]

data.frame(value = rnorm(1e4, 10, 1) ) %>% # 10.000 samples from Normal(10, 1)
    ggplot(aes(x = value) ) +
    geom_histogram(col = "white")

D’où vient la loi normale ?

Certaines valeurs sont fortement probables (autour de la moyenne \(\mu\)). Plus on s’éloigne, moins les valeurs sont probables (en suivant une décroissance exponentielle).

D’où vient la loi normale ?

\[ y = \exp \big[-x^{2} \big] \]

On étend notre fonction aux valeurs négatives.

D’où vient la loi normale ?

\[ y = \exp \big[-x^{2} \big] \]

Les points d’inflection nous donnent une bonne indication de là où la plupart des valeurs se trouvent (i.e., entre les points d’inflection). Les pics de la dérivée nous montrent les points d’inflection.

D’où vient la loi normale ?

\[ y = \exp \bigg [- \frac{1}{2} x^{2} \bigg] \]

Ensuite on standardise la distribution de manière à ce que les deux points d’inflection se trouvent à \(x = -1\) et \(x = 1\).

D’où vient la loi normale ?

\[ y = \exp \bigg [- \frac{1}{2 \color{steelblue}{\sigma^{2}}} x^{2} \bigg] \]

On insère un paramètre \(\sigma^{2}\) pour contrôler la distance entre les points d’inflection.

D’où vient la loi normale ?

\[ y = \exp \bigg [- \frac{1}{2 \color{steelblue}{\sigma^{2}}} (\color{orangered}{\mu} - x)^{2} \bigg] \]

On insère ensuite un paramètre \(\mu\) afin de pouvoir contrôler la position (la tendance centrale) de la distribution.

D’où vient la loi normale ?

\[ y = \frac{1}{\sqrt{2 \pi \color{steelblue}{\sigma^{2}}}} \exp \bigg[-\frac{1}{2 \color{steelblue}{\sigma^{2}}} (\color{orangered}{\mu} - x)^{2} \bigg] \]

Mais… cette distribution n’intègre pas à 1. On divise donc par une constante de normalisation (la partie gauche), afin d’obtenir une distribution de probabilité.

Modèle gaussien

Nous allons construire un modèle de régression, mais avant d’ajouter un prédicteur, essayons de modéliser la distribution des tailles.

On cherche à savoir quel est le modèle (la distribution) qui décrit le mieux la répartition des tailles. On va donc explorer toutes les combinaisons possibles de \(\mu\) et \(\sigma\) et les classer par leurs probabilités respectives.

Notre but, une fois encore, est de décrire la distribution postérieure, qui sera donc d’une certaine manière une distribution de distributions.

Modèle gaussien

On définit \(p(\mu, \sigma)\), la distribution a priori conjointe de tous les paramètres du modèle. On peut spécifier ces priors indépendamment pour chaque paramètre, sachant que \(p(\mu, \sigma) = p(\mu) p(\sigma)\).

\[\color{steelblue}{\mu \sim \mathrm{Normal}(178, 20)}\]

Modèle gaussien

On définit \(p(\mu, \sigma)\), la distribution a priori conjointe de tous les paramètres du modèle. On peut spécifier ces priors indépendamment pour chaque paramètre, sachant que \(p(\mu, \sigma) = p(\mu) p(\sigma)\).

\[\color{steelblue}{\sigma \sim \mathrm{Uniform}(0, 50)}\]

Visualiser le prior

library(ks)
sample_mu <- rnorm(1e4, 178, 20) # prior on mu
sample_sigma <- runif(1e4, 0, 50) # prior on sigma
prior <- data.frame(cbind(sample_mu, sample_sigma) ) # multivariate prior
H.scv <- Hscv(x = prior, verbose = TRUE)
fhat_prior <- kde(x = prior, H = H.scv, compute.cont = TRUE)
plot(
    fhat_prior, display = "persp", col = "steelblue", border = NA,
    xlab = "\nmu", ylab = "\nsigma", zlab = "\n\np(mu, sigma)",
    shade = 0.8, phi = 30, ticktype = "detailed",
    cex.lab = 1.2, family = "Helvetica")

Prior predictive checking

sample_mu <- rnorm(1000, 178, 20)
sample_sigma <- runif(1000, 0, 50)

data.frame(x = rnorm(1000, sample_mu, sample_sigma) ) %>%
    ggplot(aes(x) ) +
    geom_histogram() +
    labs(x = "Taille (en cm)", y = "Nombre d'échantillons")

Fonction de vraisemblance

mu_exemple <- 151.23
sigma_exemple <- 23.42

d2$height[34] # une observation de taille (pour exemple)
[1] 162.8648

Fonction de vraisemblance

On veut calculer la probabilité d’observer une certaine valeur de taille, sachant certaines valeurs de \(\mu\) et \(\sigma\), c’est à dire :

\[ p(x \ | \ \mu, \sigma) = \frac{1}{\sqrt{2 \pi \sigma^{2}}} \exp \bigg[-\frac{1}{2 \sigma^{2}} (\mu - x)^{2} \bigg] \]

On peut calculer cette densité de probabilité à l’aide des fonctions dnorm, dbeta, dt, dexp, dgamma, etc.

dnorm(d2$height[34], mu_exemple, sigma_exemple)
[1] 0.01505675

Fonction de vraisemblance

\[ p(x \ | \ \mu, \sigma) = \frac{1}{\sqrt{2 \pi \sigma^{2}}} \exp \bigg[-\frac{1}{2 \sigma^{2}} (\mu - x)^{2} \bigg] \]

Ou à la main…

normal_likelihood <- function (x, mu, sigma) {
  
  bell <- exp( (- 1 / (2 * sigma^2) ) * (mu - x)^2 )
  norm <- sqrt(2 * pi * sigma^2)
  
  return(bell / norm)
  
}
normal_likelihood(d2$height[34], mu_exemple, sigma_exemple)
[1] 0.01505675

Distribution postérieure

\[ \color{purple}{p(\mu, \sigma \ | \ h)} = \frac{\prod_{i} \color{orangered}{\mathrm{Normal}(h_{i} \ | \ \mu, \sigma)}\color{steelblue}{\mathrm{Normal}(\mu \ | \ 178, 20)\mathrm{Uniform}(\sigma \ | \ 0, 50)}} {\color{green}{\int \int \prod_{i} \mathrm{Normal}(h_{i} \ | \ \mu, \sigma)\mathrm{Normal}(\mu \ | \ 178, 20)\mathrm{Uniform}(\sigma \ | \ 0, 50) \mathrm{d} \mu \mathrm{d} \sigma}} \]

\[ \color{purple}{p(\mu, \sigma \ | \ h)} \propto \prod_{i} \color{orangered}{\mathrm{Normal}(h_{i} \ | \ \mu, \sigma)}\color{steelblue}{\mathrm{Normal}(\mu \ | \ 178, 20)\mathrm{Uniform}(\sigma \ | \ 0, 50)} \]

Il s’agit de la même formule vue lors des cours 1 et 2, mais cette fois en considérant qu’il existe plusieurs observations de taille (\(h_{i}\)), et deux paramètres à estimer \(\mu\) et \(\sigma\).

Pour calculer la vraisemblance marginale (en vert), il faut donc intégrer sur deux paramètres : \(\mu\) et \(\sigma\). On réalise ici encore que la probabilité a posteriori est proportionnelle au produit de la vraisemblance et du prior.

Distribution postérieure - Grid approximation

# définit une grille de valeurs possibles pour mu et sigma
mu.list <- seq(from = 140, to = 160, length.out = 200)
sigma.list <- seq(from = 4, to = 9, length.out = 200)

# étend la grille en deux dimensions (chaque combinaison de mu et sigma)
post <- expand.grid(mu = mu.list, sigma = sigma.list)

# calcul de la log-vraisemblance (pour chaque couple de mu et sigma)
post$LL <-
  sapply(
    1:nrow(post),
    function(i) sum(dnorm(
      d2$height,
      mean = post$mu[i],
      sd = post$sigma[i],
      log = TRUE)
      )
    )

# calcul de la probabilité a posteriori (non normalisée)
post$prod <-
  post$LL +
  dnorm(post$mu, 178, 20, log = TRUE) +
  dunif(post$sigma, 0, 50, log = TRUE)

# on "annule" le log en avec exp() et on standardise par la valeur maximale
post$prob <- exp(post$prod - max(post$prod) )

Distribution postérieure - Grid approximation

# select random 20 rows of the dataframe 
post %>% slice_sample(n = 20, replace = FALSE)
         mu    sigma        LL      prod          prob
1  150.8543 5.407035 -1361.703 -1370.451  1.258238e-62
2  152.1608 4.175879 -1489.783 -1498.445 3.256764e-118
3  150.1508 8.798995 -1269.760 -1278.557  1.020980e-22
4  148.0402 4.351759 -1796.177 -1805.125 2.103941e-251
5  147.2362 4.804020 -1744.958 -1753.968 3.471669e-229
6  153.7688 7.090452 -1224.601 -1233.162  5.293314e-03
7  157.1859 4.326633 -1464.064 -1472.432 6.456370e-107
8  155.5779 4.979899 -1319.606 -1328.061  3.231003e-44
9  149.6482 8.070352 -1286.217 -1295.048  7.027117e-30
10 155.6784 6.412060 -1238.425 -1246.874  5.868321e-09
11 158.9950 5.055276 -1438.713 -1446.991  7.223864e-96
12 148.0402 7.467337 -1355.539 -1364.488  4.892599e-60
13 148.0402 8.723618 -1323.578 -1332.527  3.716563e-46
14 151.0553 6.663317 -1277.742 -1286.477  3.709720e-26
15 150.4523 7.366834 -1275.967 -1284.742  2.101518e-25
16 158.3920 5.934673 -1320.970 -1329.277  9.577568e-45
17 158.9950 8.924623 -1268.749 -1277.027  4.713584e-22
18 140.5025 8.321608 -1726.117 -1735.701 2.975613e-221
19 153.3668 5.984925 -1254.417 -1263.002  5.812646e-16
20 142.7136 6.286432 -1865.703 -1875.086 8.703848e-282

Distribution postérieure - Grid approximation

sample.rows <- sample(x = 1:nrow(post), size = 1e4, replace = TRUE, prob = post$prob)

Distribution postérieure - Distributions marginales

BEST::plotPost(
  sample.mu, breaks = 40, xlab = expression(mu)
  )

Distribution postérieure - Distributions marginales

BEST::plotPost(
  sample.sigma, breaks = 40, xlab = expression(sigma)
  )

Introduction à brms

Under the hood : Stan est un langage de programmation probabiliste écrit en C++, et qui implémente plusieurs algorithmes de MCMC : HMC, NUTS, L-BFGS…

data {
  int<lower=0> J; // number of schools 
  real y[J]; // estimated treatment effects
  real<lower=0> sigma[J]; // s.e. of effect estimates 
  }

parameters {
  real mu; 
  real<lower=0> tau;
  real eta[J];
  }

transformed parameters {
  real theta[J];
  for (j in 1:J)
    theta[j] = mu + tau * eta[j];
  }

model {
  target += normal_lpdf(eta | 0, 1);
  target += normal_lpdf(y | theta, sigma);
  }

Bayesian regression models using Stan

Le package brms (Bürkner, 2017) permet de fitter des modèles multi-niveaux (ou pas) linéaires (ou pas) bayésiens en Stan mais en utilisant la syntaxe de lme4.

Par exemple, le modèle suivant :

\[ \begin{align} y_{i} &\sim \mathrm{Normal}(\mu_{i}, \sigma) \\ \mu_{i} &= \alpha + \alpha_{subject[i]} + \alpha_{item[i]} + \beta x_{i} \\ \end{align} \]

se spécifie avec brms (comme avec lme4) de la manière suivante :

model <- brm(y ~ x + (1 | subject) + (1 | item), data = d, family = gaussian() )

Rappels de syntaxe

Le package brms utilise la même syntaxe que les fonctions de base R (comme lm) ou que le package lme4.

Reaction ~ Days + (1 + Days | Subject)

La partie gauche représente notre variable dépendante (ou outcome, i.e., ce qu’on essaye de prédire). Le package brms permet également de fitter des modèles multivariés (plusieurs outcomes) en les combinant avec mvbind().

mvbind(Reaction, Memory) ~ Days + (1 + Days | Subject)

La partie droite permet de définir les prédicteurs. L’intercept est généralement implicite, de sorte que les deux écritures ci-dessous sont équivalentes.

mvbind(Reaction, Memory) ~ Days + (1 + Days | Subject)
mvbind(Reaction, Memory) ~ 1 + Days + (1 + Days | Subject)

Rappels de syntaxe

Si l’on veut fitter un modèle sans intercept (why not), il faut le spécifier explicitement comme ci-dessous.

mvbind(Reaction, Memory) ~ 0 + Days + (1 + Days | Subject)

Par défaut brms postule une vraisemblance gaussienne. Ce postulat peut être changé facilement en spécifiant la vraisemblance souhaitée via l’argument family.

brm(Reaction ~ 1 + Days + (1 + Days | Subject), family = lognormal() )

Lisez la documentation (c’est très enthousiasmant à lire) accessible via ?brm.

Quelques fonctions utiles

# générer le code du modèle en Stan
make_stancode(formula, ...)
stancode(fit)

# définir les priors
get_prior(formula, ...)
set_prior(prior, ...)

# récupérer les prédiction du modèle
fitted(fit, ...)
predict(fit, ...)
conditional_effects(fit, ...)

# posterior predictive checking
pp_check(fit, ...)

# comparaison de modèles
loo(fit1, fit2, ...)
bayes_factor(fit1, fit2, ...)
model_weights(fit1, fit2, ...)

# test d'hypothèse
hypothesis(fit, hypothesis, ...)

Un premier exemple

library(brms)
mod1 <- brm(height ~ 1, data = d2)
posterior_summary(mod1, pars = c("^b_", "sigma"), probs = c(0.025, 0.975) )
              Estimate Est.Error      Q2.5      Q97.5
b_Intercept 154.591463 0.4161782 153.77280 155.387700
sigma         7.759719 0.2841024   7.23039   8.331707

Ces données représentent les distributions marginales de chaque paramètre. En d’autres termes, la probabilité de chaque valeur de \(\mu\), après avoir moyenné sur toutes les valeurs possible de \(\sigma\), est décrite par une distribution gaussienne avec une moyenne de \(154.59\) et un écart type de \(0.41\). L’intervalle de crédibilité (\(\neq\) intervalle de confiance) nous indique les 95% valeurs de \(\mu\) ou \(\sigma\) les plus probables (sachant les données et les priors).

En utilisant notre prior

Par défaut brms utilise un prior très peu informatif centré sur la valeur moyenne de la variable mesurée. On peut donc affiner l’estimation réalisée par ce modèle en utilisant nos connaissances sur la distribution habituelle des tailles chez les humains.

La fonction get_prior() permet de visualiser une liste des priors par défaut ainsi que de tous les priors qu’on peut spécifier, sachant une certaine formule (i.e., une manière d’écrire notre modèle) et un jeu de données.

get_prior(height ~ 1, data = d2)
                    prior     class coef group resp dpar nlpar lb ub  source
 student_t(3, 154.3, 8.5) Intercept                                  default
     student_t(3, 0, 8.5)     sigma                             0    default

En utilisant notre prior

priors <- c(
  prior(normal(178, 20), class = Intercept),
  prior(exponential(0.01), class = sigma)
  )

mod2 <- brm(
  height ~ 1,
  prior = priors,
  family = gaussian(),
  data = d2
  )

En utilisant notre prior

summary(mod2)
 Family: gaussian 
  Links: mu = identity; sigma = identity 
Formula: height ~ 1 
   Data: d2 (Number of observations: 352) 
  Draws: 4 chains, each with iter = 2000; warmup = 1000; thin = 1;
         total post-warmup draws = 4000

Population-Level Effects: 
          Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
Intercept   154.60      0.42   153.80   155.42 1.00     3682     2695

Family Specific Parameters: 
      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
sigma     7.77      0.30     7.22     8.40 1.00     3932     2901

Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS
and Tail_ESS are effective sample size measures, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).

En utilisant un prior plus informatif

priors <- c(
  prior(normal(178, 0.1), class = Intercept),
  prior(exponential(0.01), class = sigma)
  )

mod3 <- brm(
  height ~ 1,
  prior = priors,
  family = gaussian(),
  data = d2
  )

En utilisant un prior plus informatif

summary(mod3)
 Family: gaussian 
  Links: mu = identity; sigma = identity 
Formula: height ~ 1 
   Data: d2 (Number of observations: 352) 
  Draws: 4 chains, each with iter = 2000; warmup = 1000; thin = 1;
         total post-warmup draws = 4000

Population-Level Effects: 
          Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
Intercept   177.87      0.10   177.67   178.06 1.00     2754     2382

Family Specific Parameters: 
      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
sigma    24.61      0.95    22.81    26.53 1.00     3011     2451

Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS
and Tail_ESS are effective sample size measures, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).

On remarque que la valeur estimée pour \(\mu\) n’a presque pas “bougée” du prior…mais on remarque également que la valeur estimée pour \(\sigma\) a largement augmentée. Nous avons dit au modèle que nous étions assez certain de notre valeur de \(\mu\), le modèle s’est ensuite “adapté”, ce qui explique la valeur de \(\sigma\)

Précision du prior (heuristique)

Le prior peut généralement être considéré comme un posterior obtenu sur des données antérieures.

On sait que le \(\sigma\) d’un posterior gaussien nous est donné par la formule :

\[\sigma_{post} = 1 / \sqrt{n}\]

Qui implique une quantité de données \(n = 1 / \sigma^2_{post}\). Notre prior avait un \(\sigma = 0.1\), ce qui donne \(n = 1 / 0.1^2 = 100\).

On peut donc considérer que le prior \(\mu \sim \mathrm{Normal}(178, 0.1)\) est équivalent au cas dans lequel nous aurions observé \(100\) tailles de moyenne \(178\).

Visualiser les échantillons de la distribution postérieure

post <- posterior_samples(mod2) %>%
    mutate(density = get_density(b_Intercept, sigma, n = 1e2) )

ggplot(post, aes(x = b_Intercept, y = sigma, color = density) ) +
    geom_point(size = 2, alpha = 0.5, show.legend = FALSE) +
    labs(x = expression(mu), y = expression(sigma) ) +
    viridis::scale_color_viridis()

Récupérer les échantillons de la distribution postérieure

# gets the first 6 samples
head(post)
  b_Intercept    sigma    lprior      lp__   density
1    154.2506 7.637362 -9.301258 -1227.089 0.6673583
2    154.7163 7.660429 -9.274108 -1226.716 1.1519070
3    154.1801 7.985184 -9.308925 -1227.476 0.6180627
4    154.1418 8.322498 -9.314582 -1228.948 0.1478108
5    154.7919 8.017387 -9.273285 -1227.154 0.8125113
6    155.5594 7.877806 -9.228092 -1229.318 0.1236258
# gets the median and the 95% credible interval
t(sapply(post[, 1:2], quantile, probs = c(0.025, 0.5, 0.975) ) )
                 2.5%        50%      97.5%
b_Intercept 153.80349 154.596669 155.420744
sigma         7.22304   7.762274   8.401343

Visualiser la distribution postérieure

H.scv <- Hscv(post[, 1:2])
fhat_post <- kde(x = post[, 1:2], H = H.scv, compute.cont = TRUE)

plot(fhat_post, display = "persp", col = "purple", border = NA,
  xlab = "\nmu", ylab = "\nsigma", zlab = "\np(mu, sigma)",
  shade = 0.8, phi = 30, ticktype = "detailed",
  cex.lab = 1.2, family = "Helvetica")

Visualiser la distribution postérieure

Ajouter un prédicteur

Comment est-ce que la taille co-varie avec le poids ?

d2 %>%
  ggplot(aes(x = weight, y = height) ) +
  geom_point(colour = "white", fill = "black", pch = 21, size = 3, alpha = 0.8)

Régression linéaire à un prédicteur continu

\[ \begin{align} h_{i} &\sim \mathrm{Normal}(\mu_{i}, \sigma) \\ \mu_{i} &= \alpha + \beta x_{i} \\ \end{align} \]

linear_model <- lm(height ~ weight, data = d2)
precis(linear_model, prob = 0.95)
                   mean         sd        2.5%       97.5%
(Intercept) 113.8793936 1.91106523 110.1337746 117.6250126
weight        0.9050291 0.04204752   0.8226175   0.9874407

Différentes notations

On considère un modèle de régression linéaire avec un seul prédicteur, une pente, un intercept, et des résidus distribués selon une loi normale. La notation :

\[ h_{i} = \alpha + \beta x_{i} + \epsilon_{i} \quad \text{avec} \quad \epsilon_{i} \sim \mathrm{Normal}(0, \sigma) \]

est équivalente à :

\[ h_{i} - (\alpha + \beta x_{i}) \sim \mathrm{Normal}(0, \sigma) \]

et si on réduit encore un peu :

\[ h_{i} \sim \mathrm{Normal}(\alpha + \beta x_{i}, \sigma). \]

Les notations ci-dessus sont équivalentes, mais la dernière est plus flexible, et nous permettra par la suite de l’étendre plus simplement aux modèles multi-niveaux.

Régression linéaire à un prédicteur continu

\[ \begin{aligned} \color{orangered}{h_{i}} \ &\color{orangered}{\sim \mathrm{Normal}(\mu_{i},\sigma)} \\ \color{black}{\mu_{i}} \ &\color{black}{= \alpha + \beta x_{i}} \\ \color{steelblue}{\alpha} \ &\color{steelblue}{\sim \mathrm{Normal}(178, 20)} \\ \color{steelblue}{\beta} \ &\color{steelblue}{\sim \mathrm{Normal}(0, 10)} \\ \color{steelblue}{\sigma} \ &\color{steelblue}{\sim \mathrm{Exponential}(0.01)} \\ \end{aligned} \]

Dans ce modèle \(\mu\) n’est plus un paramètre à estimer (car \(\mu\) est déterminé par \(\alpha\) et \(\beta\)). À la place, nous allons estimer \(\alpha\) et \(\beta\).

Rappels : \(\alpha\) est l’intercept, c’est à dire la taille attendue, lorsque le poids est égal à \(0\). \(\beta\) est la pente, c’est à dire le changement de taille attendu quand le poids augmente d’une unité.

Régression linéaire à un prédicteur continu

priors <- c(
  prior(normal(178, 20), class = Intercept),
  prior(normal(0, 10), class = b),
  prior(exponential(0.01), class = sigma)
  )

mod4 <- brm(
  height ~ 1 + weight,
  prior = priors,
  family = gaussian(),
  data = d2
  )

Régression linéaire à un prédicteur continu

posterior_summary(mod4)
                 Estimate  Est.Error          Q2.5         Q97.5
b_Intercept   113.8856101 1.89574732   110.1952619   117.5746308
b_weight        0.9049347 0.04174023     0.8245078     0.9858623
sigma           5.1051403 0.19366084     4.7430831     5.5084849
lprior        -12.4811185 0.01624770   -12.5136000   -12.4498490
lp__        -1083.3720872 1.22706415 -1086.5660511 -1081.9843281
  • \(\alpha = 113.87, 95\% \ \text{CrI} \ [110.18, 117.68]\) représente la taille moyenne quand le poids est égal à 0kg…

  • \(\beta = 0.91, 95\% \ \text{CrI} \ [0.82, 0.99]\) nous indique qu’une augmentation de 1kg entraîne une augmentation de 0.90cm.

Régression linéaire à un prédicteur continu

d2$weight.c <- d2$weight - mean(d2$weight)

mod5 <- brm(
  height ~ 1 + weight.c,
  prior = priors,
  family = gaussian(),
  data = d2
  )
fixef(mod5) # retrieves the fixed effects estimates
             Estimate  Est.Error        Q2.5       Q97.5
Intercept 154.6014518 0.26683868 154.0789683 155.1293346
weight.c    0.9056514 0.04157296   0.8261571   0.9877681

Après avoir centré la réponse, l’intercept représente désormais la valeur attendue de taille lorsque le poids est à sa valeur moyenne.

Représenter les prédictions du modèle

d2 %>%
    ggplot(aes(x = weight, y = height) ) +
    geom_point(colour = "white", fill = "black", pch = 21, size = 3, alpha = 0.8) +
    geom_abline(intercept = fixef(mod4)[1], slope = fixef(mod4)[2], lwd = 1)

Représenter l’incertitude sur \(\mu\) via fitted()

# on crée un vecteur de valeurs possibles pour "weight"
weight.seq <- data.frame(weight = seq(from = 25, to = 70, by = 1) )

# on récupère les prédictions du modèle pour ces valeurs de poids
mu <- data.frame(fitted(mod4, newdata = weight.seq) ) %>% bind_cols(weight.seq)

# on affiche les 10 premières lignes de mu
head(mu, 10)
   Estimate Est.Error     Q2.5    Q97.5 weight
1  136.5090 0.8764387 134.7750 138.2460     25
2  137.4139 0.8369078 135.7548 139.0645     26
3  138.3188 0.7976020 136.7415 139.8970     27
4  139.2238 0.7585564 137.7247 140.7202     28
5  140.1287 0.7198133 138.7078 141.5418     29
6  141.0337 0.6814243 139.6839 142.3676     30
7  141.9386 0.6434527 140.6676 143.2129     31
8  142.8435 0.6059771 141.6572 144.0390     32
9  143.7485 0.5690954 142.6247 144.8622     33
10 144.6534 0.5329310 143.6047 145.6900     34

Représenter l’incertitude sur \(\mu\) via fitted()

d2 %>%
  ggplot(aes(x = weight, y = height) ) +
  geom_point(colour = "white", fill = "black", pch = 21, size = 3, alpha = 0.8) +
  geom_smooth(
    data = mu, aes(y = Estimate, ymin = Q2.5, ymax = Q97.5),
    stat = "identity",
    color = "black", alpha = 0.8, size = 1
    )

Intervalles de prédiction (incorporer \(\sigma\))

Pour rappel, voici notre modèle : \(h_{i} \sim \mathrm{Normal}(\alpha + \beta x_{i}, \sigma)\). Pour l’instant, on a seulement représenté les prédictions pour \(\mu\). Comment incorporer \(\sigma\) dans nos prédictions ?

# on crée un vecteur de valeurs possibles pour "weight"
weight.seq <- data.frame(weight = seq(from = 25, to = 70, by = 1) )

# on récupère les prédictions du modèle pour ces valeurs de poids
pred_height <- data.frame(predict(mod4, newdata = weight.seq) ) %>% bind_cols(weight.seq)

# on affiche les 10 premières lignes de pred_height
head(pred_height, 10)
   Estimate Est.Error     Q2.5    Q97.5 weight
1  136.5820  5.096479 126.8537 146.7209     25
2  137.3847  5.147709 127.3273 147.3246     26
3  138.3392  5.135458 128.2959 148.2815     27
4  139.3207  5.121729 129.4048 149.4566     28
5  140.2142  5.091009 130.1698 150.0758     29
6  140.9824  5.131876 130.5004 150.7245     30
7  141.8691  5.058856 132.1439 151.8404     31
8  142.8151  5.056535 132.7018 152.6379     32
9  143.6872  5.072205 133.7425 153.5068     33
10 144.5605  5.111002 134.5023 154.3200     34

Intervalles de prédiction (incorporer \(\sigma\))

d2 %>%
  ggplot(aes(x = weight, y = height) ) +
  geom_point(colour = "white", fill = "black", pch = 21, size = 3, alpha = 0.8) +
  geom_ribbon(
    data = pred_height, aes(x = weight, ymin = Q2.5, ymax = Q97.5),
    alpha = 0.2, inherit.aes = FALSE
    ) +
  geom_smooth(
    data = mu, aes(y = Estimate, ymin = Q2.5, ymax = Q97.5),
    stat = "identity", color = "black", alpha = 0.8, size = 1
    )

Deux types d’incertitude

Deux sources d’incertitude dans le modèle : incertitude concernant l’estimation de la valeur des paramètres mais également concernant le processus d’échantillonnage.

Incertitude épistémique : La distribution a posteriori ordonne toutes les combinaisons possibles des valeurs des paramètres selon leurs plausibilités relatives.

Incertitude aléatoire : La distribution des données simulées est elle, une distribution qui contient de l’incertitude liée à un processus d’échantillonnage (i.e., générer des données à partir d’une gaussienne).

Voir aussi ce court article par O’Hagan (2004).

Régression polynomiale

d %>% # on utilise d au lieu de d2
  ggplot(aes(x = weight, y = height) ) +
  geom_point(colour = "white", fill = "black", pch = 21, size = 3, alpha = 0.8)

Scores standardisés

d <- d %>% mutate(weight.s = (weight - mean(weight) ) / sd(weight) )

d %>%
    ggplot(aes(x = weight.s, y = height) ) +
    geom_point(colour = "white", fill = "black", pch = 21, size = 3, alpha = 0.8)
c(mean(d$weight.s), sd(d$weight.s) )
[1] -2.712698e-18  1.000000e+00

Scores standardisés

Pourquoi standardiser les prédicteurs ?

  • Interprétation. Permet de comparer les coefficients de plusieurs prédicteurs. Un changement d’un écart-type du prédicteur correspond à un changement d’un écart-type sur la réponse (si la réponse est aussi standardisée).

  • Fitting. Quand les prédicteurs contiennent de grandes valeurs, cela peut poser des problèmes de convergence (cf. Cours n°05)…

Modèle de régression polynomiale - exercice

\[ \begin{aligned} \color{orangered}{h_{i}} \ &\color{orangered}{\sim \mathrm{Normal}(\mu_{i}, \sigma)} \\ \color{black}{\mu_{i}} \ &\color{black}{= \alpha + \beta_{1} x_{i} + \beta_{2} x_{i}^{2}} \\ \color{steelblue}{\alpha} \ &\color{steelblue}{\sim \mathrm{Normal}(156, 100)} \\ \color{steelblue}{\beta_{1}, \beta_{2}} \ &\color{steelblue}{\sim \mathrm{Normal}(0, 10)} \\ \color{steelblue}{\sigma} \ &\color{steelblue}{\sim \mathrm{Exponential}(0.01)} \\ \end{aligned} \]

À vous de construire et fitter ce modèle en utilisant brms::brm().

Modèle de régression polynomiale

priors <- c(
  prior(normal(156, 100), class = Intercept),
  prior(normal(0, 10), class = b),
  prior(exponential(0.01), class = sigma)
  )

mod6 <- brm(
  # NB: polynomials should be written with the I() function...
  height ~ 1 + weight.s + I(weight.s^2),
  prior = priors,
  family = gaussian(),
  data = d
  )

Modèle de régression polynomiale

summary(mod6)
 Family: gaussian 
  Links: mu = identity; sigma = identity 
Formula: height ~ 1 + weight.s + I(weight.s^2) 
   Data: d (Number of observations: 544) 
  Draws: 4 chains, each with iter = 2000; warmup = 1000; thin = 1;
         total post-warmup draws = 4000

Population-Level Effects: 
            Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
Intercept     146.66      0.38   145.89   147.40 1.00     3656     2876
weight.s       21.41      0.29    20.84    21.99 1.00     3507     2755
Iweight.sE2    -8.41      0.28    -8.97    -7.85 1.00     3231     2815

Family Specific Parameters: 
      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
sigma     5.79      0.18     5.44     6.16 1.00     3847     2988

Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS
and Tail_ESS are effective sample size measures, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).

Représenter les prédictions du modèle

# on crée un vecteur de valeurs possibles pour "weight"
weight.seq <- data.frame(weight.s = seq(from = -2.5, to = 2.5, length.out = 50) )

# on récupère les prédictions du modèle pour ces valeurs de poids
mu <- data.frame(fitted(mod6, newdata = weight.seq) ) %>% bind_cols(weight.seq)
pred_height <- data.frame(predict(mod6, newdata = weight.seq) ) %>% bind_cols(weight.seq)

# on affiche les 10 premières lignes de pred_height
head(pred_height, 10)
   Estimate Est.Error     Q2.5     Q97.5  weight.s
1  40.57030  5.915044 28.63565  52.31376 -2.500000
2  47.04358  5.962782 35.52740  58.92465 -2.397959
3  53.19093  5.768899 42.09591  64.62317 -2.295918
4  59.11254  5.800214 47.82035  70.36574 -2.193878
5  64.99005  5.789996 53.66561  76.22579 -2.091837
6  70.77512  5.797930 59.26238  81.91836 -1.989796
7  76.27559  5.767157 64.86341  87.59925 -1.887755
8  81.83142  5.831812 70.04876  93.43039 -1.785714
9  86.63248  5.899853 75.40574  98.30328 -1.683673
10 91.74488  5.844115 79.92874 102.94806 -1.581633

Représenter les prédictions du modèle

d %>%
  ggplot(aes(x = weight.s, y = height) ) +
  geom_point(colour = "white", fill = "black", pch = 21, size = 3, alpha = 0.8) +
  geom_ribbon(
    data = pred_height, aes(x = weight.s, ymin = Q2.5, ymax = Q97.5),
    alpha = 0.2, inherit.aes = FALSE
    ) +
  geom_smooth(
    data = mu, aes(y = Estimate, ymin = Q2.5, ymax = Q97.5),
    stat = "identity", color = "black", alpha = 0.8, size = 1
    )

Modèle de régression, taille d’effet

Plusieurs méthodes pour calculer les tailles d’effet dans les modèles bayésiens. Gelman & Pardoe (2006) proposent une méthode pour calculer un \(R^{2}\) basé sur l’échantillon.

Marsman & Wagenmakers (2017) et Marsman et al. (2019) généralisent des méthodes existantes pour calculer un \(\rho^{2}\) pour les designs de type ANOVA (i.e., avec prédicteurs catégoriels), qui représente une estimation de la taille d’effet dans la population, et non basé sur l’échantillon.

Similar to most of the ES measures that have been proposed for the ANOVA model, the squared multiple correlation coefficient \(\rho^{2}\) […] is a so-called proportional reduction in error measure (PRE). In general, a PRE measure expresses the proportion of the variance in an outcome \(y\) that is attributed to the independent variables \(x\) (Marsman et al., 2019).

Modèle de régression, taille d’effet

\[ \begin{aligned} \rho^{2} &= \dfrac{\sum_{i = 1}^{n} \pi_{i}(\beta_{i} - \beta)^{2}}{\sigma^{2} + \sum_{i=1}^{n} \pi_{i}(\beta_{i} - \beta)^{2}} \\ \rho^{2} &= \dfrac{ \frac{1}{n} \sum_{i=1}^{n} \beta_{i}^{2}}{\sigma^{2} + \frac{1}{n} \sum_{i = 1}^{n} \beta_{i}^{2}} \\ \rho^{2} &= \dfrac{\beta^{2} \tau^{2}}{\sigma^{2} + \beta^{2} \tau^{2}}\\ \end{aligned} \]

post <- posterior_samples(mod4)
beta <- post$b_weight
sigma <- post$sigma

f1 <- beta^2 * var(d2$weight)
rho <- f1 / (f1 + sigma^2)

Attention, si plusieurs prédicteurs, dépend de la structure de covariance…

Modèle de régression, taille d’effet

BEST::plotPost(rho, showMode = TRUE, xlab = expression(rho) )
summary(lm(height ~ weight, data = d2) )$r.squared
[1] 0.5696444

Modèle de régression, taille d’effet

bayes_R2(mod4)
    Estimate  Est.Error      Q2.5     Q97.5
R2 0.5682439 0.02278088 0.5208918 0.6084963
BEST::plotPost(bayes_R2(mod4, summary = FALSE), showMode = TRUE, xlab = expression(rho) )

Résumé du cours

On a présenté un nouveau modèle à deux puis trois paramètres : le modèle gaussien, puis la régression linéaire gaussienne, permettant de mettre en relation deux variables continues.

Comme précédemment, le théorème de Bayes est utilisé pour mettre à jour nos connaissances a priori quant à la valeur des paramètres en une connaissance a posteriori, synthèse entre nos priors et l’information contenue dans les données.

La package brms permet de fitter toutes sortes de modèles avec une syntaxe similaire à celle utilisée par lm().

La fonction fitted() permet de récupérer les prédictions d’un modèle fitté avec brms (i.e., un modèle de classe brmsfit).

La fonction predict() permet de simuler des données à partir d’un modèle fitté avec brms.

Travaux pratiques - 1/2

Sélectionner toutes les lignes du jeu de données Howell1 correspondant à des individus mineurs (age < 18). Cela devrait résulter en une dataframe de 192 lignes.

Fitter un modèle de régression linéaire en utilisant la fonction brms::brm(). Reporter et interpréter les estimations de ce modèle. Pour une augmentation de 10 unités de weight, quelle augmentation de taille (height) le modèle prédit-il ?

Faire un plot des données brutes avec le poids sur l’axe des abscisses et la taille sur l’axe des ordonnées. Surimposer la droite de régression du modèle et un intervalle de crédibilité à 89% pour la moyenne. Ajouter un intervalle de crédibilité à 89% pour les tailles prédites.

Que pensez-vous du fit du modèle ? Quelles conditions d’application du modèle seriez-vous prêt.e.s à changer, afin d’améliorer le modèle ?

Travaux pratiques - 2/2

Imaginons que vous ayez consulté une collègue experte en allométrie (i.e., les phénomènes de croissance différentielle d’organes) et que cette dernière vous explique que ça ne fait aucun sens de modéliser la relation entre le poids et la taille… alors qu’on sait que c’est le logarithme du poids qui est relié à la taille !

Modéliser alors la relation entre la taille (cm) et le log du poids (log-kg). Utiliser la dataframe Howell1 en entier (les 544 lignes). Fitter le modèle suivant en utilisant brms::brm().

\[ \begin{align*} &\color{orangered}{h_{i} \sim \mathrm{Normal}(\mu_{i}, \sigma)} \\ &\mu_{i}= \alpha + \beta \cdot \log (w_{i}) \\ &\color{steelblue}{\alpha \sim \mathrm{Normal}(178, 100)} \\ &\color{steelblue}{\beta \sim \mathrm{Normal}(0, 100)} \\ &\color{steelblue}{\sigma \sim \mathrm{Exponential}(0.01)} \\ \end{align*} \]

\(h_{i}\) est la taille de l’individu \(i\) et \(w_{i}\) le poids de l’individu \(i\). La fonction pour calculer le log en R est simplement log(). Est-ce que vous savez interpréter les résultats ? Indice: faire un plot des données brutes et surimposer les prédictions du modèle…

Proposition de solution

data(Howell1)

# on garde seulement les individus ayant moins de 18 ans
d <- Howell1 %>% filter(age < 18)

priors <- c(
  prior(normal(150, 100), class = Intercept),
  prior(normal(0, 10), class = b),
  prior(exponential(0.01), class = sigma)
  )

mod7 <- brm(
  height ~ 1 + weight,
  prior = priors,
  family = gaussian(),
  data = d
  )

Proposition de solution

summary(mod7, prob = 0.89)
 Family: gaussian 
  Links: mu = identity; sigma = identity 
Formula: height ~ 1 + weight 
   Data: d (Number of observations: 192) 
  Draws: 4 chains, each with iter = 2000; warmup = 1000; thin = 1;
         total post-warmup draws = 4000

Population-Level Effects: 
          Estimate Est.Error l-89% CI u-89% CI Rhat Bulk_ESS Tail_ESS
Intercept    58.26      1.41    55.97    60.47 1.00     4302     3141
weight        2.72      0.07     2.61     2.83 1.00     4313     3036

Family Specific Parameters: 
      Estimate Est.Error l-89% CI u-89% CI Rhat Bulk_ESS Tail_ESS
sigma     8.53      0.43     7.86     9.24 1.00     3693     2850

Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS
and Tail_ESS are effective sample size measures, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).

Représenter les prédictions du modèle

# on crée un vecteur de valeurs possibles pour "weight"
weight.seq <- data.frame(weight = seq(from = 5, to = 45, length.out = 1e2) )

# on récupère les prédictions du modèle pour ces valeurs de poids
mu <- data.frame(
  fitted(mod7, newdata = weight.seq, probs = c(0.055, 0.945) )
  ) %>%
  bind_cols(weight.seq)

pred_height <- data.frame(
  predict(mod7, newdata = weight.seq, probs = c(0.055, 0.945) )
  ) %>%
  bind_cols(weight.seq)

# on affiche les 6 premières lignes de pred_height
head(pred_height)
  Estimate Est.Error     Q5.5    Q94.5   weight
1 72.01941  8.546671 58.43185 85.81827 5.000000
2 73.09154  8.713609 59.30663 86.93305 5.404040
3 73.97282  8.655970 60.16455 87.94621 5.808081
4 75.36751  8.675354 61.44540 89.16854 6.212121
5 76.15794  8.620021 62.21609 89.89341 6.616162
6 77.20407  8.609109 63.36282 91.03503 7.020202

Représenter les prédictions du modèle

d %>%
  ggplot(aes(x = weight, y = height) ) +
  geom_point(colour = "white", fill = "black", pch = 21, size = 3, alpha = 0.8) +
  geom_ribbon(
    data = pred_height, aes(x = weight, ymin = Q5.5, ymax = Q94.5),
    alpha = 0.2, inherit.aes = FALSE
    ) +
  geom_smooth(
    data = mu, aes(y = Estimate, ymin = Q5.5, ymax = Q94.5),
    stat = "identity", color = "black", alpha = 0.8, size = 1
    )

Proposition de solution

# on considère maintenant tous les individus
d <- Howell1

mod8 <- brm(
  # on prédit la taille par le logarithme du poids
  height ~ 1 + log(weight),
  prior = priors,
  family = gaussian(),
  data = d
  )

Proposition de solution

summary(mod8, prob = 0.89)
 Family: gaussian 
  Links: mu = identity; sigma = identity 
Formula: height ~ 1 + log(weight) 
   Data: d (Number of observations: 544) 
  Draws: 4 chains, each with iter = 2000; warmup = 1000; thin = 1;
         total post-warmup draws = 4000

Population-Level Effects: 
          Estimate Est.Error l-89% CI u-89% CI Rhat Bulk_ESS Tail_ESS
Intercept   -23.53      1.38   -25.77   -21.34 1.00     3560     2590
logweight    47.00      0.40    46.38    47.65 1.00     3543     2257

Family Specific Parameters: 
      Estimate Est.Error l-89% CI u-89% CI Rhat Bulk_ESS Tail_ESS
sigma     5.16      0.16     4.91     5.41 1.00     4265     3184

Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS
and Tail_ESS are effective sample size measures, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).

Représenter les prédictions du modèle

# on crée un vecteur de valeurs possibles pour "weight"
weight.seq <- data.frame(weight = seq(from = 5, to = 65, length.out = 1e2) )

# on récupère les prédictions du modèle pour ces valeurs de poids
mu <- data.frame(
  fitted(mod8, newdata = weight.seq, probs = c(0.055, 0.945) )
  ) %>%
  bind_cols(weight.seq)

pred_height <- data.frame(
  predict(mod8, newdata = weight.seq, probs = c(0.055, 0.945) )
  ) %>%
  bind_cols(weight.seq)

# on affiche les 6 premières lignes de pred_height
head(pred_height)
  Estimate Est.Error     Q5.5    Q94.5   weight
1 52.03217  5.179091 43.82956 60.54144 5.000000
2 57.60610  5.232138 49.00986 65.77798 5.606061
3 62.38594  5.265352 54.01512 70.86161 6.212121
4 66.77034  5.154598 58.64690 75.05824 6.818182
5 70.78297  5.273320 62.14081 79.12357 7.424242
6 74.41589  5.257686 65.97590 82.79756 8.030303

Représenter les prédictions du modèle

d %>%
  ggplot(aes(x = weight, y = height) ) +
  geom_point(colour = "white", fill = "black", pch = 21, size = 3, alpha = 0.8) +
  geom_ribbon(
    data = pred_height, aes(x = weight, ymin = Q5.5, ymax = Q94.5),
    alpha = 0.2, inherit.aes = FALSE
    ) +
  geom_smooth(
    data = mu, aes(y = Estimate, ymin = Q5.5, ymax = Q94.5),
    stat = "identity", color = "black", alpha = 0.8, size = 1
    )

Références

Bürkner, P.-C. (2017). brms : An R Package for Bayesian Multilevel Models Using Stan. Journal of Statistical Software, 80(1). https://doi.org/10.18637/jss.v080.i01
Gelman, A., & Pardoe, I. (2006). Bayesian measures of explained variance and pooling in multilevel (hierarchical) models. Technometrics, 48(2), 241–251. https://doi.org/10.1198/004017005000000517
Marsman, M., & Wagenmakers, E.-J. (2017). Three Insights from a Bayesian Interpretation of the One-Sided P Value. Educational and Psychological Measurement, 77(3), 529–539. https://doi.org/10.1177/0013164416669201
Marsman, M., Waldorp, L., Dablander, F., & Wagenmakers, E.-J. (2019). Bayesian estimation of explained variance in ANOVA designs. Statistica Neerlandica, 0(0), 1–22. https://doi.org/10.1111/stan.12173
O’Hagan, T. (2004). Dicing with the unknown. Significance, 1(3), 132–133. https://doi.org/10.1111/j.1740-9713.2004.00050.x